// Icon cell renderer
// Copyright 2006 (C) Ralph Thomas

#include <view/gtk/flyweight/cell-renderer-icon.h>
#include <egg-pixbuf-thumbnail.h>
#include <boost/shared_ptr.hpp>
#include <util/thread.h>
#include <boost/thread/mutex.hpp>
#include <string>
#include <list>
#include <algorithm>
#include <toolkit/application.h>
#include <boost/filesystem/path.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/bind.hpp>
#include <boost/signals.hpp>
#include <map>

#include <iostream>

namespace bfs = boost::filesystem;

/* This is based mainly on GtkCellRendererIcon
 *  in GAIM, written and (c) 2002 by Sean Egan
 *  (Licensed under the GPL), which in turn is
 *  based on Gtk's GtkCellRenderer[Text|Toggle|Pixbuf]
 *  implementation by Jonathan Blandford */

/* Some boring function declarations: GObject type system stuff */

static void     mission_cell_renderer_icon_init       (MissionCellRendererIcon      *cellicon);

static void     mission_cell_renderer_icon_class_init (MissionCellRendererIconClass *klass);

static void     mission_cell_renderer_icon_get_property  (GObject                    *object,
                                                             guint                       param_id,
                                                             GValue                     *value,
                                                             GParamSpec                 *pspec);

static void     mission_cell_renderer_icon_set_property  (GObject                    *object,
                                                             guint                       param_id,
                                                             const GValue               *value,
                                                             GParamSpec                 *pspec);

static void     mission_cell_renderer_icon_finalize (GObject *gobject);


/* These functions are the heart of our mission cell renderer: */

static void     mission_cell_renderer_icon_get_size   (GtkCellRenderer            *cell,
                                                          GtkWidget                  *widget,
                                                          GdkRectangle               *cell_area,
                                                          gint                       *x_offset,
                                                          gint                       *y_offset,
                                                          gint                       *width,
                                                          gint                       *height);

static void     mission_cell_renderer_icon_render     (GtkCellRenderer            *cell,
                                                          GdkDrawable                *window,
                                                          GtkWidget                  *widget,
                                                          GdkRectangle               *background_area,
                                                          GdkRectangle               *cell_area,
                                                          GdkRectangle               *expose_area,
                                                          GtkCellRendererState       flags);


enum
{
  PROP_ICON_NAME = 1,
  PROP_INDEX,
  PROP_SIZE,
};

static   gpointer parent_class;


/***************************************************************************
 *
 *  mission_cell_renderer_icon_get_type: here we register our type with
 *                                          the GObject type system if we
 *                                          haven't done so yet. Everything
 *                                          else is done in the callbacks.
 *
 ***************************************************************************/

GType
mission_cell_renderer_icon_get_type (void)
{
  static GType cell_icon_type = 0;

  if (cell_icon_type)
    return cell_icon_type;

  if (1)
  {
    static const GTypeInfo cell_icon_info =
    {
      sizeof (MissionCellRendererIconClass),
      NULL,                                                     /* base_init */
      NULL,                                                     /* base_finalize */
      (GClassInitFunc) mission_cell_renderer_icon_class_init,
      NULL,                                                     /* class_finalize */
      NULL,                                                     /* class_data */
      sizeof (MissionCellRendererIcon),
      0,                                                        /* n_preallocs */
      (GInstanceInitFunc) mission_cell_renderer_icon_init,
    };

    /* Derive from GtkCellRenderer */
    cell_icon_type = g_type_register_static (GTK_TYPE_CELL_RENDERER,
                                                 "MissionCellRendererIcon",
                                                  &cell_icon_info,
                                                  (GTypeFlags)0);
  }

  return cell_icon_type;
}


/***************************************************************************
 *
 *  mission_cell_renderer_icon_init: set some default properties of the
 *                                      parent (GtkCellRenderer).
 *
 ***************************************************************************/

static void
mission_cell_renderer_icon_init (MissionCellRendererIcon *cellrenderericon)
{
  GTK_CELL_RENDERER(cellrenderericon)->mode = GTK_CELL_RENDERER_MODE_INERT;
  GTK_CELL_RENDERER(cellrenderericon)->xpad = 2;
  GTK_CELL_RENDERER(cellrenderericon)->ypad = 2;
  GTK_CELL_RENDERER(cellrenderericon)->yalign = 0.0;
  GTK_CELL_RENDERER(cellrenderericon)->xalign = 0.0;
  cellrenderericon->data = new MissionCellRendererIconPrivate();
  cellrenderericon->data->index_m = 0;
  cellrenderericon->data->size_m = 128;
}


/***************************************************************************
 *
 *  mission_cell_renderer_icon_class_init:
 *
 *  set up our own get_property and set_property functions, and
 *  override the parent's functions that we need to implement.
 *  And make our new "percentage" property known to the type system.
 *  If you want cells that can be activated on their own (ie. not
 *  just the whole row selected) or cells that are editable, you
 *  will need to override 'activate' and 'start_editing' as well.
 *
 ***************************************************************************/

static void
mission_cell_renderer_icon_class_init (MissionCellRendererIconClass *klass)
{
  GtkCellRendererClass *cell_class   = GTK_CELL_RENDERER_CLASS(klass);
  GObjectClass         *object_class = G_OBJECT_CLASS(klass);

  parent_class           = g_type_class_peek_parent (klass);
  object_class->finalize = mission_cell_renderer_icon_finalize;

  /* Hook up functions to set and get our
   *   mission cell renderer properties */
  object_class->get_property = mission_cell_renderer_icon_get_property;
  object_class->set_property = mission_cell_renderer_icon_set_property;

  /* Override the two crucial functions that are the heart
   *   of a cell renderer in the parent class */
  cell_class->get_size = mission_cell_renderer_icon_get_size;
  cell_class->render   = mission_cell_renderer_icon_render;

  /* Install our very own properties */
  g_object_class_install_property (object_class,
                                   PROP_ICON_NAME,
                                   g_param_spec_string ("icon-name",
                                                        "Icon name",
                                                         "The name of the icon, or filename to display",
                                                         NULL,
                                                         (GParamFlags)G_PARAM_READWRITE));
  g_object_class_install_property (object_class,
		  PROP_INDEX,
		  g_param_spec_int ("index", "Index", "The index of this item, used for invalidation", 0, INT_MAX, 0, (GParamFlags)G_PARAM_READWRITE));

  g_object_class_install_property (object_class,
		  PROP_SIZE,
		  g_param_spec_int ("size", "Size", "The width and height to use for this item", 0, INT_MAX, 128, (GParamFlags)G_PARAM_READWRITE));
}


/***************************************************************************
 *
 *  mission_cell_renderer_icon_finalize: free any resources here
 *
 ***************************************************************************/

static void
mission_cell_renderer_icon_finalize (GObject *object)
{
  MissionCellRendererIcon *cellrenderericon = MISSION_CELL_RENDERER_ICON(object);
  delete cellrenderericon->data;

  /* Free any dynamically allocated resources here */

//  (* G_OBJECT_CLASS (parent_class)->finalize) (object);
}


/***************************************************************************
 *
 *  mission_cell_renderer_icon_get_property: as it says
 *
 ***************************************************************************/

static void
mission_cell_renderer_icon_get_property (GObject    *object,
                                            guint       param_id,
                                            GValue     *value,
                                            GParamSpec *psec)
{
  MissionCellRendererIcon  *cellicon = MISSION_CELL_RENDERER_ICON(object);

  switch (param_id)
  {
    case PROP_ICON_NAME:
      g_value_set_string (value, cellicon->data->icon_m.c_str());
      break;

	case PROP_INDEX:
	  g_value_set_int (value, cellicon->data->index_m);
	  break;

	case PROP_SIZE:
	  g_value_set_int (value, cellicon->data->size_m);
	  break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, psec);
      break;
  }
}


/***************************************************************************
 *
 *  mission_cell_renderer_icon_set_property: as it says
 *
 ***************************************************************************/

static void
mission_cell_renderer_icon_set_property (GObject      *object,
                                            guint         param_id,
                                            const GValue *value,
                                            GParamSpec   *pspec)
{
  MissionCellRendererIcon *cellicon = MISSION_CELL_RENDERER_ICON (object);

  switch (param_id)
  {
    case PROP_ICON_NAME:
      cellicon->data->icon_m = g_value_get_string(value);
      break;

	case PROP_INDEX:
	  cellicon->data->index_m = g_value_get_int(value);
	  break;

	case PROP_SIZE:
	  cellicon->data->size_m = g_value_get_int(value);
	  break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, param_id, pspec);
      break;
  }
}

/***************************************************************************
 *
 *  mission_cell_renderer_icon_new: return a new cell renderer instance
 *
 ***************************************************************************/

GtkCellRenderer *
mission_cell_renderer_icon_new (void)
{
  return reinterpret_cast<GtkCellRenderer*>(g_object_new(MISSION_TYPE_CELL_RENDERER_ICON, NULL));
}

void
mission_cell_renderer_icon_set_notify_function(GtkCellRenderer* object, const boost::function<void (unsigned int)>& notify)
{
	MissionCellRendererIcon* cellicon = MISSION_CELL_RENDERER_ICON (object);
	cellicon->data->inval_m = notify;
}
/***************************************************************************
 *
 *  mission_cell_renderer_icon_get_size: crucial - calculate the size
 *                                          of our cell, taking into account
 *                                          padding and alignment properties
 *                                          of parent.
 *
 ***************************************************************************/

//#define FIXED_WIDTH   128
//#define FIXED_HEIGHT  128

static void
mission_cell_renderer_icon_get_size (GtkCellRenderer *cell,
                                        GtkWidget       *widget,
                                        GdkRectangle    *cell_area,
                                        gint            *x_offset,
                                        gint            *y_offset,
                                        gint            *width,
                                        gint            *height)
{
  MissionCellRendererIcon*	  cellicon = MISSION_CELL_RENDERER_ICON (cell);
  gint calc_width;
  gint calc_height;

  calc_width  = (gint) cellicon->data->size_m;
  calc_height = (gint) cellicon->data->size_m;

  if (width)
    *width = calc_width;

  if (height)
    *height = calc_height;

  if (cell_area)
  {
    if (x_offset)
    {
      *x_offset = (gint)(cell->xalign * (cell_area->width - calc_width));
      *x_offset = MAX (*x_offset, 0);
    }

    if (y_offset)
    {
      *y_offset = (gint)(cell->yalign * (cell_area->height - calc_height));
      *y_offset = MAX (*y_offset, 0);
    }
  }
}

struct lru_cache_t {
	struct thumb_cache_entry_t {
		GdkPixbuf*	pixbuf_m;
		std::string	filename_m;

		thumb_cache_entry_t(const std::string& filename, GdkPixbuf* pixbuf) : pixbuf_m(pixbuf), filename_m(filename) {
			if (pixbuf_m) g_object_ref(pixbuf_m);
		}
		thumb_cache_entry_t() : pixbuf_m(0) {}
		thumb_cache_entry_t(const thumb_cache_entry_t& rhs) : pixbuf_m(rhs.pixbuf_m), filename_m(rhs.filename_m) {
			if (pixbuf_m) g_object_ref(pixbuf_m);
		}
		~thumb_cache_entry_t() {
			if (pixbuf_m) g_object_unref(pixbuf_m);
		}
		bool operator==(const std::string& s) { return s == filename_m; }
	};
	typedef std::list<thumb_cache_entry_t> list_t;

	list_t							lru_list_m;
	toolkit::monitor_connection_t	file_monitor_m;
	boost::signal<void ()>			invalidate_signal_m;

	void invalidate(const std::string& uri) {
		//
		// If we have a cache of this filename then remove it. The next
		// time the list view draws it will be invalid.
		//
		gchar* filename_c(g_filename_from_uri(uri.c_str(), 0, 0));
		std::string filename(filename_c);
		g_free(filename_c);
		for (list_t::iterator i = lru_list_m.begin(); i != lru_list_m.end(); i++) {
			if (i->filename_m == filename) {
				lru_list_m.erase(i);
				invalidate_signal_m();
				return;
			}
		}
	}

	GdkPixbuf* lookup(const std::string& filename) {
		list_t::iterator i = std::find(lru_list_m.begin(), lru_list_m.end(), filename);
		if (i == lru_list_m.end()) return 0;
		if (i != lru_list_m.begin()) {
			//
			// Remove it and insert it at the front.
			//
			thumb_cache_entry_t te(*i);
			lru_list_m.erase(i);
			lru_list_m.push_front(te);
			g_object_ref(te.pixbuf_m);
			return te.pixbuf_m;
		}
		//
		// It is at the front already.
		//
		g_object_ref(i->pixbuf_m);
		return i->pixbuf_m;
	}
	void insert(const std::string& filename, GdkPixbuf* pixbuf) {
		//
		// Insert at the front.
		//
		lru_list_m.push_front(thumb_cache_entry_t(filename, pixbuf));
		if (lru_list_m.size() > 32)
			lru_list_m.pop_back();
		//
		// If we're not monitoring a directory for changes then
		// monitor this one.
		//
		// XXX: Should support monitoring more than a single
		// XXX: directory!
		//
		if (!file_monitor_m)
			{
			bfs::path p(filename, bfs::native);
			gchar* uri(g_filename_to_uri(p.branch_path().native_file_string().c_str(), 0, 0));
			file_monitor_m = toolkit::application::getInstance().monitor( uri, boost::bind( &lru_cache_t::invalidate, this, _1 ), true );
			g_free(uri);
			}
	}
	typedef boost::shared_ptr<lru_cache_t> lru_cache_ptr_t;
	static lru_cache_ptr_t get_instance() {
		static lru_cache_ptr_t ptr;
		if (!ptr) ptr = lru_cache_ptr_t(new lru_cache_t());
		return ptr;
	}
};

boost::signals::connection mission_cell_renderer_icon_inval(const boost::function<void ()>& f) {
	return lru_cache_t::get_instance()->invalidate_signal_m.connect(f);
}

struct thumbnail_load_queue_t {
	typedef boost::shared_ptr<util::thread> thread_ptr_t;
	typedef boost::function<void ()>	load_notify_function_t;

	static void create_thumbnail(const std::string& filename, const load_notify_function_t& load_notify) {
		//
		// This does all the work of creating a thumbnail and writing
		// it to disk (or recording a failure).
		//
		GdkPixbuf* thumb = egg_pixbuf_get_thumbnail_for_file( filename.c_str(), EGG_PIXBUF_THUMBNAIL_NORMAL, NULL );
		if (thumb) g_object_unref(thumb);
		if (load_notify) toolkit::application::getInstance().executeInMain(load_notify);
	}
	static void thumbnail(const std::string& filename, const load_notify_function_t& load_notify) {
		static thread_ptr_t thread;
		if (!thread) thread = thread_ptr_t(new util::thread());
		thread->adoptWork(boost::bind( &thumbnail_load_queue_t::create_thumbnail, filename, load_notify ));
	}
};

struct icon_cache_t {
	typedef std::map<std::string, GdkPixbuf*>	icon_map_t;
	typedef boost::shared_ptr<icon_cache_t>		icon_cache_ptr_t;
	icon_map_t	icon_cache_m;

	~icon_cache_t() {
		for( icon_map_t::const_iterator i = icon_cache_m.begin(); i != icon_cache_m.end(); i++ )
			g_object_unref(i->second);
	}

	GdkPixbuf* lookup(const std::string& icon_name) {
		icon_map_t::const_iterator i = icon_cache_m.find(icon_name);
		if (i != icon_cache_m.end()) {
			g_object_ref(i->second);
			return i->second;
		}
		GdkPixbuf* icon = gtk_icon_theme_load_icon( gtk_icon_theme_get_default(), icon_name.c_str(), 128, (GtkIconLookupFlags)0, 0 );
		icon_cache_m[icon_name] = icon;
		g_object_ref(icon);
		return icon;
	}

	static GdkPixbuf* load_icon(const std::string& icon_name) {
		static icon_cache_ptr_t	icon_cache_s;
		if (!icon_cache_s) icon_cache_s = icon_cache_ptr_t(new icon_cache_t());
		return icon_cache_s->lookup(icon_name);
	}
};

/***************************************************************************
 *
 *  mission_cell_renderer_icon_render: crucial - do the rendering.
 *
 ***************************************************************************/

static void
mission_cell_renderer_icon_render (GtkCellRenderer *cell,
                                      GdkDrawable     *window,
                                      GtkWidget       *widget,
                                      GdkRectangle    *background_area,
                                      GdkRectangle    *cell_area,
                                      GdkRectangle    *expose_area,
                                      GtkCellRendererState            flags)
{
  MissionCellRendererIcon*	  cellicon = MISSION_CELL_RENDERER_ICON (cell);
  GtkStateType                state = GTK_STATE_NORMAL;
  gint                        width = 0, height = 0;
  gint                        x_offset = 0, y_offset = 0;


  if (cellicon->data->icon_m.empty()) return;

  mission_cell_renderer_icon_get_size (cell, widget, cell_area,
                                          &x_offset, &y_offset,
                                          &width, &height);

  if (GTK_WIDGET_HAS_FOCUS (widget))
    state = GTK_STATE_ACTIVE;

  char* actual_fn = g_filename_from_uri(cellicon->data->icon_m.c_str(), 0, 0);
  if (!actual_fn) {
	std::cout << "null from uri: " << cellicon->data->icon_m << std::endl;
	return;
  }
  char* actual_uri = g_filename_to_uri(actual_fn, 0, 0);
  bfs::path path(actual_fn, bfs::native);
  GdkPixbuf* thumb = 0;
  if (actual_fn) {
	thumb = lru_cache_t::get_instance()->lookup( actual_fn );
	if (!thumb) {
		thumb = egg_pixbuf_load_thumbnail( actual_uri, bfs::last_write_time(path), EGG_PIXBUF_THUMBNAIL_NORMAL );
		if (!thumb) {
			std::string fallback_icon_name = "gnome-fs-loading-icon";
			//
			// If we have a failed icon then load up the "failed icon" icon.
			//
			if (egg_pixbuf_has_failed_thumbnail( actual_uri, bfs::last_write_time( path ), NULL )) {
				fallback_icon_name = "gtk-image-missing";
			} else thumbnail_load_queue_t::thumbnail( path.native_file_string(), boost::bind( cellicon->data->inval_m, cellicon->data->index_m ) );
			//
			// Load the "timer icon" while we wait.
			//
			thumb = icon_cache_t::load_icon(fallback_icon_name);
		} else lru_cache_t::get_instance()->insert( path.native_file_string(), thumb );
	}
  	g_free(actual_fn);
	g_free(actual_uri);
  }
  if (thumb) {
	  //
	  // Center when drawing.
	  //
	  int size = cellicon->data->size_m;
	  int xoff = cell_area->x + x_offset + cell->xpad;
	  int yoff = cell_area->y + y_offset + cell->ypad;
	  int w = (gdk_pixbuf_get_width(thumb) * size) / 128;
	  int h = (gdk_pixbuf_get_height(thumb) * size) / 128;
	  xoff += (width - w) / 2;
	  yoff += (height - h) / 2;
	  GdkPixbuf* pb = thumb;
	  if (size != 128)
		pb = gdk_pixbuf_scale_simple(thumb, w, h, GDK_INTERP_NEAREST);
	  gdk_draw_pixbuf(window, widget->style->black_gc, pb, 0, 0, xoff, yoff, -1, -1, GDK_RGB_DITHER_NONE, 0, 0);
	  g_object_unref(thumb);
	  if (pb != thumb) g_object_unref(pb);
  }
}

